2. Manual Base-Class Serialization
Combining class hierarchies and serialization,
whether fully automatic or custom, is straightforward: all classes
either use only the Serializable attribute, or use the attribute and also implement ISerializable. However, the picture isn't so clear when it comes to deriving a serializable class from a class not marked with the Serializable attribute, as in this case:
public class MyBaseClass
{}
[Serializable]
public class MySubClass : MyBaseClass
{}
In fact, in such a case, .NET can't serialize objects of type MySubClass at all, because it can't serialize their base classes. Trying to serialize an object of type MySubClass results in an exception of type SerializationException.
Such a situation may occur when deriving from a class in a third-party
assembly where the vendor neglected to mark its class as serializable.
The good news is that there is a workaround for
such a case. The solution presented here isn't a sure cure, because it
assumes that none of the base classes require custom serialization
steps. It merely compensates for the oversight of not marking the base
class as serializable.
The workaround is simple: the subclass can implement ISerializable,
use reflection to read and serialize the base classes' fields, and use
reflection again to set these fields during deserialization. The static SerializationUtil helper class provides the two static methods SerializeBaseType( ) and DeserializeBaseType( ), defined as:
public static class SerializationUtil
{
public static void SerializeBaseType(object obj,
SerializationInfo info,
StreamingContext context);
public static void DeserializeBaseType(object obj,
SerializationInfo info,
StreamingContext context);
//Rest of SerializationUtil
}
All the subclass needs to do is implement ISerializable and use SerializationUtil to serialize and deserialize its base classes:
public class MyBaseClass
{}
[Serializable]
public class MySubClass : MyBaseClass,ISerializable
{
public MySubClass( )
{}
public void GetObjectData(SerializationInfo info,StreamingContext context)
{
SerializationUtil.SerializeBaseType(this,info,context);
}
protected MySubClass(SerializationInfo info,StreamingContext context)
{
SerializationUtil.DeserializeBaseType(this,info,context);
}
}
If the subclass itself has no need for custom serialization and only implements ISerializable to serialize its base class, you can use SerializationUtil to serialize the subclass as well. SerializationUtil provides these overloaded versions of SerializeBaseType( ) and DeserializeBaseType( ):
public static void SerializeBaseType(object obj,bool
serializeSelf,
SerializationInfo info,
StreamingContext context);
public static void DeserializeBaseType(object obj,bool
deserializeSelf,
SerializationInfo info,
StreamingContext context);
These versions accept a flag instructing them whether to start serialization with the type itself instead of its base class:
public void GetObjectData(SerializationInfo info,StreamingContext context)
{
//Serializing this type and its base classes
SerializationUtil.SerializeBaseType(this,true,info,context);
}
protected MyClass(SerializationInfo info,StreamingContext context)
{
//Deserializing this type and its base classes
SerializationUtil.DeserializeBaseType(this,true,info,context);
}
SerializationUtil is also useful in cases where a class needs to provide custom serialization, even though simple use of the Serializable
attribute would have sufficed for that class. This may occur because,
as mentioned previously, if any class in a class hierarchy provides
custom serialization, all its subclasses must do so as well. It can also
occur when a type is constrained to implement Iserializable, to be used as a generic type parameter in a generic class. |
|
Example 2 demonstrates the implementations of SerializeBaseType and DeserializeBaseType.
Example 2. Implementing SerializeBaseType and DeserializeBaseType
public static class SerializationUtil
{
public static void SerializeBaseType(object obj,
SerializationInfo info,StreamingContext context)
{
Type baseType = obj.GetType( ).BaseType;
SerializeBaseType(obj,baseType,info,context);
}
static void SerializeBaseType(object obj,Type type,SerializationInfo info,
StreamingContext context)
{
if(type == typeof(object))
{
return;
}
BindingFlags flags = BindingFlags.Instance|BindingFlags.DeclaredOnly|
BindingFlags.NonPublic|BindingFlags.Public;
FieldInfo[] fields = type.GetFields(flags);
foreach(FieldInfo field in fields)
{
if(field.IsNotSerialized)
{
continue;
}
string fieldName = type.Name + "+" + field.Name;
info.AddValue(fieldName,field.GetValue(obj));
}
SerializeBaseType(obj,type.BaseType,info,context);
}
public static void DeserializeBaseType(object obj,SerializationInfo info,
StreamingContext context)
{
Type baseType = obj.GetType( ).BaseType;
DeserializeBaseType(obj,baseType,info,context);
}
static void DeserializeBaseType(object obj,Type type,SerializationInfo info,
StreamingContext context)
{
if(type == typeof(object))
{
return;
}
BindingFlags flags = BindingFlags.Instance|BindingFlags.DeclaredOnly|
BindingFlags.NonPublic|BindingFlags.Public;
FieldInfo[] fields = type.GetFields(flags);
foreach(FieldInfo field in fields)
{
if(field.IsNotSerialized)
{
continue;
}
string fieldName = type.Name + "+" + field.Name;
object fieldValue = info.GetValue(fieldName,field.FieldType);
field.SetValue(obj,fieldValue);
}
DeserializeBaseType(obj,type.BaseType,info,context);
}
//Rest of SerializationUtil
}
|
When SerializationUtil serializes an
object's base class, it needs to serialize all the base classes leading
to that base class as well. You can access the base-class type using the
BaseType property of Type:
Type baseType = obj.GetType( ).BaseType;
With the GetFields( ) method of Type,
you can get all the fields (private and public) declared by the type,
as well as any public or protected fields available via its own base
classes. This isn't good enough for serialization, though, because you
need to capture all the private fields available from all levels of the
class hierarchy, including ones with the same name. The solution is to
serialize each level of the class hierarchy separately and thus access
each level's private fields. SerializeBaseType( ) calls a private helper method, also called SerializeBaseType( ), providing it with the level of the class hierarchy to serialize:
SerializeBaseType(obj,baseType,info,context);
The private SerializeBaseType( ) serializes that level and then calls itself recursively, serializing the next level up the hierarchy:
SerializeBaseType(obj,type.BaseType,info,context);
The recursion stops once it reaches the System.Object level:
static void SerializeBaseType(object obj,Type type,SerializationInfo info,
StreamingContext context)
{
if(type == typeof(object))
{
return;
}
/* Rest of the implementation */
}
To serialize a particular level, the private SerializeBaseType( ) calls GetFields( ) with a binding flags mask (BindingFlags.DeclaredOnly),
which instructs it to return all fields defined by this type only and
not its base types. This ensures that as it visits the next levels up
the hierarchy, it doesn't end up serializing fields more than once. It
also binds to instance and not static fields, because static fields are
never serialized:
BindingFlags flags = BindingFlags.Instance|BindingFlags.DeclaredOnly|
BindingFlags.NonPublic|BindingFlags.Public;
The private SerializeBaseType( ) then calls GetFields( ) and stores the result in an array of FieldInfo objects:
FieldInfo[] fields = type.GetFields(flags);
This solution needs to deal with a class hierarchy in which some levels actually use the Serializable attribute, such as class A in this example:
[Serializable]
class A
{}
class B : A
{}
[Serializable]
class C : B,ISerializable
{...}
Because class A may contain some fields marked with the NonSerialized attribute, the solution needs to check that the fields are serializable. This is easy to do via the IsNotSerialized Boolean property of FieldInfo:
foreach(FieldInfo field in fields)
{
if(field.IsNotSerialized)
{
continue;
}
//Rest of the iteration loop
}
Since different levels can declare private fields with the same names in the same class hierarchy, the private SerializeBaseType( ) prefixes each field name with its declaring type separated by a +:
string fieldName = type.Name + "+" + field.Name;
The value of a field is obtained via the GetValue( ) method of FieldInfo and is then added to the info parameter:
info.AddValue(fieldName,field.GetValue(obj));
Deserialization of the base class (or classes) is similar to serialization and is also done recursively until the System.Object level is reached. The public DeserializeBaseType( ) method accesses the base type and calls the private helper method DeserializeBaseType( ). At each level in the class hierarchy, the private DeserializeBaseType( )
retrieves the collection of fields for that type. For each field, it
creates a name by appending the name of the current level to the name of
the field, gets the value from info, and sets the value of the corresponding field, using the SetValue( ) method of the FieldInfo class:
string fieldName = type.Name + "+" + field.Name;
object fieldValue =
info.GetValue(fieldName,field.FieldType);field.SetValue(obj,fieldValue);